using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Oops.Xna.Framework.GameState;

namespace Oops.Xna.Framework.Testing.App
{
	public delegate bool PromptCallback(int selectedOption, GameTime gameTime, ref GameScreen newScreen);

	public enum PromptResult
	{
		None,
		Accept,
		Cancel
	}

	public class PromptScreen : GameScreen
	{
		// The states of the prompt screen.
		private enum PromptState
		{
			TransitionIn,
			Active,
			TransitionOut,
			Exit
		}

		// Transition timings.
		private static TimeSpan TransitionInTime = TimeSpan.FromSeconds(0.4D);
		private static TimeSpan TransitionOutTime = TimeSpan.FromSeconds(0.4D);

		private string text;
		private string[] lines;

		private Rectangle currentBounds;
		private Rectangle maximumBounds;

		private PromptState state;
		private PromptResult result = PromptResult.None;

		private int gamePadIndex = -1;

		private int selectedOption = 0;
		private List<string> options = new List<string>();

		private PromptCallback callback;

		private TimeSpan elapsedTime;

		// Graphic resources.
		private SpriteBatch spriteBatch;
		private SpriteFont promptFont;
		private Texture2D promptTexture;

		// Audio resources.
		private WaveBank waveBank;
		private SoundBank soundBank;

		public PromptScreen(Game game) : base(game, "Prompt") { }

		public void Initialize(string text, PromptCallback callback, params string[] options)
		{
			// Assert parameters.
			Debug.Assert(callback != null);

			// Hold onto text and parse into lines.
			this.text = text;
			this.lines = this.text.Split('|');

			// Hold onto options.  The default is an "OK" option.
			this.options.Clear();
			this.options.AddRange(options);
			if (this.options.Count == 0)
			{
				this.options.Add("OK");
			}

			// Hold onto callback.  This delegate is called when an option is chosen.
			this.callback = callback;

			// Determine the maximun width of the prompt bounds.
			this.maximumBounds.Width = 0;
			for (int i = 0; i < this.lines.Length; i++)
			{
				this.maximumBounds.Width = Math.Max(this.maximumBounds.Width, Convert.ToInt32(this.promptFont.MeasureString(this.lines[i]).X));
			}
			for (int i = 0; i < this.options.Count; i++)
			{
				this.maximumBounds.Width = Math.Max(this.maximumBounds.Width, Convert.ToInt32(this.promptFont.MeasureString(this.options[i]).X));
			}
			this.maximumBounds.Width += Convert.ToInt32(this.promptFont.MeasureString("W").X) << 1;

			// Determine the maximum height of the prompt bounds.
			this.maximumBounds.Height = (this.lines.Length + this.options.Count + 3) * this.promptFont.LineSpacing;

			// Center the prompt bounds around the center of the screen.
			this.maximumBounds.X = this.GraphicsDevice.Viewport.TitleSafeArea.Left + (this.GraphicsDevice.Viewport.TitleSafeArea.Width >> 1) - (this.maximumBounds.Width >> 1);
			this.maximumBounds.Y = this.GraphicsDevice.Viewport.TitleSafeArea.Top + (this.GraphicsDevice.Viewport.TitleSafeArea.Height >> 1) - (this.maximumBounds.Height >> 1);
		}

		public PromptResult Result
		{
			get { return this.result; }
		}

		public int SelectedOption
		{
			get { return this.selectedOption; }
		}

		#region GameScreen Members

		public override void Enter(GameTime gameTime, ref GameScreen previousScreen)
		{
			// Initialize state.
			this.state = PromptState.TransitionIn;

			// Reset flags.
			this.result = PromptResult.None;
			this.selectedOption = 0;

			// Initialize time for animation effects.
			this.elapsedTime = TimeSpan.Zero;

			// Play entry sound.
			this.soundBank.PlayCue("Enter");

			// Call inherited method.
			base.Enter(gameTime, ref previousScreen);
		}

		public override bool GetTransition(GameTime gameTime, ref GameScreen newScreen)
		{
			// Transition is based on prompt's result.
			if (this.state == PromptState.Exit)
			{
				return this.callback(this.result == PromptResult.Accept ? this.SelectedOption : -1, gameTime, ref newScreen);
			}

			// Update should always be called.
			return true;
		}

		#endregion

		#region DrawableGameComponent Members

		protected override void LoadContent()
		{
			// Create the sprite the batch.
			this.spriteBatch = new SpriteBatch(this.GraphicsDevice);

			// Get the graphic resources from the global content provider.
			this.promptFont = this.Game.GetContent().Load<SpriteFont>(@".\Content\Fonts\prompt");
			this.promptTexture = this.Game.GetContent().Load<Texture2D>(@".\Content\Textures\prompt");

			// Call inherited method.
			base.LoadContent();
		}

		protected override void UnloadContent()
		{
			// Dispose of the graphic resources.
			if (this.spriteBatch != null)
			{
				this.spriteBatch.Dispose();
			}

			// Call inherited method.
			base.UnloadContent();
		}

		#endregion

		#region IGameComponent Members

		public override void Initialize()
		{
			// Call inherited method.
			base.Initialize();

			// Create the wave bank and sound bank fthat contain the sound effects for the prompt.
			this.waveBank = new WaveBank(this.AudioService.AudioEngine, @".\Content\Audio\Prompt.xwb");
			this.soundBank = new SoundBank(this.AudioService.AudioEngine, @".\Content\Audio\Prompt.xsb");
		}

		#endregion

		#region IUpdateable Members

		public override void Update(GameTime gameTime)
		{
			// Update the elapsed time.
			this.elapsedTime += gameTime.ElapsedGameTime;

			switch (this.state)
			{
				case PromptState.TransitionIn:
					// Determine the percent of the animation completion and if the options should be displayed.
					float inPercentage = (float)(this.elapsedTime.TotalSeconds / PromptScreen.TransitionInTime.TotalSeconds);
					if (inPercentage >= 1.0f)
					{
						this.elapsedTime -= PromptScreen.TransitionInTime;
						this.state = PromptState.Active;
					}

					// Cap size of message box at maximum bounds.
					inPercentage = MathHelper.Clamp(inPercentage, 0.001f, 1.0f);

					// Calculate the message box bounds at this point in the animation.
					this.currentBounds = this.maximumBounds;
					this.currentBounds.Inflate(
						-Convert.ToInt32((this.maximumBounds.Width >> 1) - ((this.maximumBounds.Width >> 1) * inPercentage)),
						-Convert.ToInt32((this.maximumBounds.Height >> 1) - ((this.maximumBounds.Height >> 1) * inPercentage)));

					break;

				case PromptState.Active:
					// Determine the game pads that are going to be polled.
					int startIndex = this.gamePadIndex == -1 ? 0 : this.gamePadIndex;
					int endIndex = this.gamePadIndex == -1 ? 3 : this.gamePadIndex;

					// Iterate on game pad indices.
					for (int i = startIndex; i <= endIndex; i++)
					{
						// Check for controller connection status.
						//if (this.InputService.IsGamePadConnected(i))
						{
							// Check for cancellation.
							if (this.InputService.IsKeyPressed(Keys.Escape) ||
								this.InputService.IsGamePadButtonPressed(i, Buttons.B) ||
								this.InputService.IsGamePadButtonPressed(i, Buttons.Back))
							{
								// Set the result of selection.
								this.result = PromptResult.Cancel;

								// Update screen state.
								this.elapsedTime = TimeSpan.Zero;
								this.state = PromptState.TransitionOut;

								// Play cancel sound.
								this.soundBank.PlayCue("Exit");

								break;
							}
							else
							{
								// Change the selected option to the next option
								if (this.InputService.IsKeyPressed(Keys.W) ||
									this.InputService.IsKeyPressed(Keys.Up) ||
									this.InputService.IsGamePadButtonPressed(i, Buttons.DPadUp) ||
									(this.InputService.GetGamePadLeftThumbStick(i).Y > 0 && this.InputService.GetGamePadLeftThumbStickPrevious(i).Y <= 0))
								{
									int newSelection = this.selectedOption - 1;
									if (newSelection < 0)
									{
										newSelection = this.options.Count - 1;
									}

									if (this.selectedOption != newSelection)
									{
										this.selectedOption = newSelection;
										this.soundBank.PlayCue("Selection");
									}

									break;
								}
								// Change the selected option to the previous option.
								else if (this.InputService.IsKeyPressed(Keys.S) ||
									this.InputService.IsKeyPressed(Keys.Down) ||
									this.InputService.IsGamePadButtonPressed(i, Buttons.DPadDown) ||
									(this.InputService.GetGamePadLeftThumbStick(i).Y < 0 && this.InputService.GetGamePadLeftThumbStickPrevious(i).Y >= 0))
								{
									int newSelection = (this.selectedOption + 1) % this.options.Count;
									if (this.selectedOption != newSelection)
									{
										this.selectedOption = newSelection;
										this.soundBank.PlayCue("Selection");
									}

									break;
								}
								// The selection was accepted.
								else if (this.InputService.IsKeyPressed(Keys.Enter) ||
									this.InputService.IsGamePadButtonPressed(i, Buttons.A))
								{
									// Set the result of selection.
									this.result = PromptResult.Accept;

									// Update screen state.
									this.elapsedTime = TimeSpan.Zero;
									this.state = PromptState.TransitionOut;

									// Play accept sound.
									this.soundBank.PlayCue("Exit");

									break;
								}
							}
						}
					}

					break;

				case PromptState.TransitionOut:
					// Determine the percent of the animation completion and if the options should be displayed.
					float outPercentage = 1.0f - (float)(this.elapsedTime.TotalSeconds / PromptScreen.TransitionOutTime.TotalSeconds);
					if (outPercentage <= 0.0f)
					{
						this.elapsedTime -= PromptScreen.TransitionOutTime;
						this.state = PromptState.Exit;
					}

					// Cap size of message box at maximum bounds.
					outPercentage = MathHelper.Clamp(outPercentage, 0.001f, 1.0f);

					// Calculate the message box bounds at this point in the animation.
					this.currentBounds = this.maximumBounds;
					this.currentBounds.Inflate(
						-Convert.ToInt32((this.maximumBounds.Width >> 1) - ((this.maximumBounds.Width >> 1) * outPercentage)),
						-Convert.ToInt32((this.maximumBounds.Height >> 1) - ((this.maximumBounds.Height >> 1) * outPercentage)));

					break;
			}

			// Call inherited method.
			base.Update(gameTime);
		}

		#endregion

		#region IDrawable Members

		public override void Draw(GameTime gameTime)
		{
			// Start batch processing.
			this.spriteBatch.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.Immediate, SaveStateMode.SaveState);

			// Draw the prompt border in the correct size.
			if (this.state != PromptState.Exit)
			{
				this.spriteBatch.Draw(this.promptTexture, this.currentBounds, Color.White);
			}

			// Draw all text items.
			if (this.state == PromptState.Active)
			{
				// Draw the text.
				Vector2 position = new Vector2();
				position.Y = this.maximumBounds.Top + this.promptFont.LineSpacing;
				for (int i = 0; i < this.lines.Length; i++)
				{
					if (!String.IsNullOrEmpty(this.lines[i]))
					{
						// Update the horizontal position by the line's width and draw the line.
						position.X = this.maximumBounds.Left + (this.maximumBounds.Width >> 1) - (this.promptFont.MeasureString(this.lines[i]).X * 0.5f);
						this.spriteBatch.DrawString(this.promptFont, this.lines[i], position, Color.White);
					}

					// Update the vertical position.
					position.Y += this.promptFont.LineSpacing;
				}

				// There's space between the text and options.
				position.Y += this.promptFont.LineSpacing;

				// Draw the options.
				position.X = this.maximumBounds.Left + (this.maximumBounds.Width >> 1);
				for (int i = 0; i < this.options.Count; i++)
				{
					float scale;
					Color color;

					// Determine the scale and color of the current option.
					if (i == this.selectedOption)
					{
						float pulsate = (float)Math.Sin(gameTime.TotalGameTime.TotalSeconds * 6.0D) + 1.0f;
						scale = 1.0f + pulsate * 0.05f;
						color = Color.Yellow;
					}
					else
					{
						scale = 1.0f;
						color = Color.White;
					}

					// Determine the origin of the current option.
					Vector2 origin = new Vector2(this.promptFont.MeasureString(this.options[i]).X * 0.5f, this.promptFont.LineSpacing >> 1);

					// Draw the current option.
					this.spriteBatch.DrawString(
						this.promptFont,
						this.options[i],
						position,
						color,
						0.0f,
						origin,
						scale,
						SpriteEffects.None,
						0);

					// Update the vertical position.
					position.Y += this.promptFont.LineSpacing;
				}
			}

			// End the batch processing.
			this.spriteBatch.End();

			// Call inherited method.
			base.Draw(gameTime);
		}

		#endregion
	}
}
